/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.IO;
using System.Xml;
using System.Windows.Forms;
using Microsoft.Win32;
using System.Xml.Xsl;

namespace DaveSexton.DocProject.InstallActions
{
	internal sealed class ContentInstaller
	{
		#region Public Properties
		#endregion

		#region Private / Protected
		private string TemplateSource
		{
			get
			{
				return Path.Combine(target, templateSetupDirectory);
			}
		}

		private string AddinSource
		{
			get
			{
				return Path.Combine(target, addinSetupDirectory);
			}
		}

		private const string templateSetupDirectory = @"Setup\Project Templates";
		private const string addinSetupDirectory = @"Setup\Addin";

		[NonSerialized]
		private readonly IList<string> installedTemplates = new List<string>(16);
		[NonSerialized]
		private readonly IList<string> installedAddins = new List<string>(4);
		[NonSerialized]
		private readonly IList<string> updatedVisualStudioConfigurations = new List<string>(4);

		private readonly bool allUsers;
		private readonly string target, addinAssemblyName;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="ContentInstaller" /> class.
		/// </summary>
		public ContentInstaller(string target, string addinAssemblyName, bool allUsers)
		{
			if (target == null)
				throw new ArgumentNullException("target");

			if (addinAssemblyName == null)
				throw new ArgumentNullException("addinAssemblyName");

			this.allUsers = allUsers;
			this.target = target;
			this.addinAssemblyName = addinAssemblyName;
		}
		#endregion

		#region Methods
		public void Install(IEnumerable<VisualStudioContent> contents)
		{
			if (contents == null)
				throw new ArgumentNullException("contents");

			bool hasValidContent = false;
			int count = 0;

			foreach (VisualStudioContent content in contents)
			{
				count++;
				content.Initialize(allUsers);

				if (content.IsValid)
				{
					hasValidContent = true;

					if (content.HasAddin)
						InstallAddin(content);

					InstallTemplates(content);

					UpdateVisualStudioConfiguration(content, true);

					if (allUsers)
						UpdateVisualStudio(content);
				}
			}

			if (count > 0 && !hasValidContent)
				throw new InvalidOperationException(Resources.Errors.InstallVSNotFound);
		}

		private void InstallAddin(VisualStudioContent content)
		{
			string addinTarget = content.AddinFullPath;

			if (installedAddins.Contains(addinTarget))
				return;

			string source = Path.Combine(Path.Combine(AddinSource, content.VisualStudioYear), content.Addin);
			string targetDir = Path.GetDirectoryName(addinTarget);

			UpdateAddin(source, content.VisualStudioVersionRegistryKey);

			if (!Directory.Exists(targetDir))
				Directory.CreateDirectory(targetDir);

			File.Copy(source, addinTarget, true);

			installedAddins.Add(addinTarget);
		}

		private void InstallTemplates(VisualStudioContent content)
		{
			foreach (Template template in content.Templates)
			{
				string templateTarget = content.GetTemplatePath(template);

				if (installedTemplates.Contains(templateTarget))
					continue;

				string source = Path.Combine(Path.Combine(TemplateSource, content.VisualStudioYear), template.Name);
				string targetDir = Path.GetDirectoryName(templateTarget);

				if (!Directory.Exists(targetDir))
					Directory.CreateDirectory(targetDir);

				File.Copy(source, templateTarget, true);

				installedTemplates.Add(templateTarget);
			}
		}

		private void UpdateAddin(string addin, string visualStudioVersion)
		{
			try
			{
				using (StringReader reader = new StringReader(Transformations.DocProject_AddIn))
				{
					using (XmlReader stylesheet = XmlReader.Create(reader))
					{
						XmlWriterSettings settings = new XmlWriterSettings();
						settings.Encoding = Encoding.Unicode;

						using (StringWriter writer = new StringWriter())
						{
							XsltArgumentList arguments = new XsltArgumentList();
							arguments.AddParam("AddinAssemblyPath", "", Path.Combine(Path.Combine(target, @"bin"), addinAssemblyName));
							arguments.AddParam("AddinAssemblyName", "", typeof(DaveSexton.DocProject.DocProjectAddIn).Assembly.FullName);

							XslCompiledTransform transform = new XslCompiledTransform();
							transform.Load(stylesheet);
							transform.Transform(addin, arguments, writer);

							File.WriteAllText(addin, writer.ToString(), Encoding.Unicode);
						}
					}
				}
			}
			catch (FileNotFoundException ex)
			{
				throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Resources.Errors.InstallAddinFileNotFound, addin), ex);
			}
			catch (XmlException ex)
			{
				throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Resources.Errors.InstallAddinNotWellformedXml, addin, Environment.NewLine, ex.Message), ex);
			}
			catch (Exception ex)
			{
				throw new InvalidOperationException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
					Resources.Errors.InstallAddinGeneralLoadError, addin, Environment.NewLine, ex.Message), ex);
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		public void Uninstall(IEnumerable<VisualStudioContent> contents)
		{
			if (contents == null)
				throw new ArgumentNullException("contents");

			foreach (VisualStudioContent content in contents)
			{
				if (!content.IsValid)
					continue;

				UninstallProviders(content);

				if (content.HasAddin)
				{
					try
					{
						File.Delete(content.AddinFullPath);
					}
					catch (IOException) { }
					catch (UnauthorizedAccessException) { }
				}

				foreach (Template template in content.Templates)
				{
					try
					{
						File.Delete(content.GetTemplatePath(template));

						if (!string.IsNullOrEmpty(template.Category))
						{
							string path = content.GetTemplateCategoryPath(template);

							// (throws IOException if it's not empty)
							Directory.Delete(path, false);
						}
					}
					catch (IOException) { }
					catch (UnauthorizedAccessException) { }
				}

				UpdateVisualStudioConfiguration(content, false);

				if (allUsers)
				{
					try
					{
						UpdateVisualStudio(content);
					}
					catch (Exception ex)
					// in case VS has been uninstalled
					{
						Log.Exception(ex, Resources.Errors.VSNotFound);
					}
				}
			}
		}

		private static void UpdateVisualStudio(VisualStudioContent content)
		{
			using (System.Diagnostics.Process process = new System.Diagnostics.Process())
			{
				process.StartInfo.FileName = Path.Combine(content.VisualStudioInstallPath, content.VisualStudioProgramName);
				process.StartInfo.Arguments = "/InstallVSTemplates";
				process.Start();

				// wait one minute; in testing, VS would sometimes remain open for over 30 seconds.
				process.WaitForExit(60 * 1000);
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private void UpdateVisualStudioConfiguration(VisualStudioContent content, bool installing)
		{
			if (content.VisualStudioYear != "2008" || updatedVisualStudioConfigurations.Contains("2008"))
				return;
			else
				updatedVisualStudioConfigurations.Add("2008");

			try
			{
				string config = Path.Combine(content.VisualStudioInstallPath, content.VisualStudioProgramName) + ".config";

				XmlDocument document = OpenConfigFile(config, installing);

				if (document == null)	// this can only happen when installing is false
					return;

				XmlNamespaceManager ns = CustomActions.GetXmlNamespaceManagerForAssemblyBindingConfig(document);

				XmlNode assemblyBinding = CustomActions.GetAssemblyBindingNode(document, installing);

				if (assemblyBinding == null)
					return;

				bool changed = false;

				if (installing)
					changed = CustomActions.AddBindingRedirect(ns, document, assemblyBinding, "Microsoft.VisualStudio.VCProjectEngine", "b03f5f7f11d50a3a", "neutral", "8.0.0.0", "9.0.0.0");
				else
					changed = CustomActions.RemoveBindingRedirect(ns, assemblyBinding, "Microsoft.VisualStudio.VCProjectEngine");

				if (changed)
				{
					document.Save(config);

					Log.Warn("Visual Studio configuration file updated.\r\n\r\n{0} VCProjectEngine assembly binding redirect from version 8.0.0.0 to 9.0.0.0.\r\n\r\nFile: {1}",
						(installing) ? "Added" : "Removed", config);
				}
			}
			catch (Exception ex)
			{
				Log.Exception(ex, Resources.Errors.VSConfigUpdateError, (installing) ? "installation" : "uninstallation");
			}
		}

		private static XmlDocument OpenConfigFile(string file, bool canCreate)
		{
			XmlDocument document;

			try
			{
				document = new XmlDocument();
				document.Load(file);
				return document;
			}
			catch (DirectoryNotFoundException) { }
			catch (FileNotFoundException) { }

			if (!canCreate)
				return null;

			document = new XmlDocument();
			document.AppendChild(document.CreateElement("configuration"));
			return document;
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private static void UninstallProviders(VisualStudioContent content)
		{
			try
			{
				string progid = content.VisualStudioRegistryKey + ".DTE." + content.VisualStudioVersionRegistryKey;

				UninstallProvidersInVisualStudio(progid);
			}
			catch (Exception ex)
			// in case VS has been uninstalled
			{
				Log.Exception(ex, Resources.Errors.VSAssemblyNotFound);
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private static void UninstallProvidersInVisualStudio(string progid)
		{
			Type type = Type.GetTypeFromProgID(progid, false);

			if (type == null)
				return;

			EnvDTE80.DTE2 dte = null;

			try
			{
				dte = (EnvDTE80.DTE2) Activator.CreateInstance(type);

				if (dte != null)
				{
					DocProjectEnvironment.IsUninstalling = true;		// internal flag
					DocProjectEnvironment.Initialize(dte);
					DocProjectEnvironment.Uninitialize();		// handles uninstallation of providers and VS commands
				}
			}
			catch (Exception ex)
			{
				Log.Exception(ex, Resources.Errors.UninstallProvidersError, Environment.NewLine, "[pre-uninstall]", progid);
			}
			finally
			{
				if (dte != null)
					dte.Quit();
			}
		}
		#endregion
	}
}
